package tcln.gxtx.client.widget.treegrid;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import tcln.gxtx.client.layout.ResizableLayoutContainer;
import tcln.gxtx.client.utils.Predicate;
import tcln.gxtx.client.utils.ChangeTrackable.ChangeState;
import tcln.gxtx.client.utils.assertion.GwtAssert;
import tcln.gxtx.client.utils.async.DeferredCommandScheduler;
import tcln.gxtx.client.utils.lang.GwtObjectUtils;
import tcln.gxtx.client.utils.lang.GwtStringUtils;
import tcln.gxtx.client.utils.store.StoreUtils;
import tcln.gxtx.client.widget.grid.renderer.DefaultGridCellRenderer;
import tcln.gxtx.client.widget.treegrid.model.AbstractTreeModel;
import tcln.gxtx.client.widget.treegrid.renderer.DefaultTreeGridCellRenderer;
import tcln.gxtx.client.widget.treegrid.utils.DefaultTreeGridModelDataFormatter;
import tcln.gxtx.client.widget.treegrid.utils.TreeGridModelDataFormatter;

import com.extjs.gxt.ui.client.Style.HorizontalAlignment;
import com.extjs.gxt.ui.client.Style.SelectionMode;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.data.ModelKeyProvider;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.EditorEvent;
import com.extjs.gxt.ui.client.event.EventType;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.GridEvent;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.TreeGridEvent;
import com.extjs.gxt.ui.client.state.StateManager;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.store.Store;
import com.extjs.gxt.ui.client.store.StoreEvent;
import com.extjs.gxt.ui.client.store.StoreFilter;
import com.extjs.gxt.ui.client.store.TreeStore;
import com.extjs.gxt.ui.client.store.TreeStoreEvent;
import com.extjs.gxt.ui.client.store.Record.RecordUpdate;
import com.extjs.gxt.ui.client.widget.ComponentPlugin;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.Editor;
import com.extjs.gxt.ui.client.widget.LayoutContainer;
import com.extjs.gxt.ui.client.widget.grid.CellEditor;
import com.extjs.gxt.ui.client.widget.grid.CellSelectionModel;
import com.extjs.gxt.ui.client.widget.grid.ColumnConfig;
import com.extjs.gxt.ui.client.widget.grid.ColumnModel;
import com.extjs.gxt.ui.client.widget.grid.GridCellRenderer;
import com.extjs.gxt.ui.client.widget.grid.GridSelectionModel;
import com.extjs.gxt.ui.client.widget.grid.GridView;
import com.extjs.gxt.ui.client.widget.layout.FitLayout;
import com.extjs.gxt.ui.client.widget.treegrid.EditorTreeGrid;
import com.extjs.gxt.ui.client.widget.treegrid.TreeGrid;
import com.extjs.gxt.ui.client.widget.treegrid.TreeGridCellRenderer;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Element;

/**
 * Abstract class for a treegrid. This class is, in fact, a {@link LayoutContainer} which encapsulates either a
 * read-only treegrid ( {@link TreeGrid} ) or an editable one ( {@link EditorTreeGrid} ). On constructing this treegrid,
 * user is allowed to specify this parameter.
 * 
 * @param <M>
 *            Tree model type.
 * @author Truong Chau Lien Ngoc (lienngoc@gmail.com)
 */
public abstract class AbstractTreeGrid<M extends AbstractTreeModel> extends ResizableLayoutContainer {

    /**
     * The inner treegrid of this treegrid.
     */
    protected TreeGrid<M> tree;

    /**
     * The content panel containing the tree.
     */
    protected ContentPanel treeContentPanel;

    /**
     * Treestore of this treegrid.
     */
    protected TreeStore<M> treeStore;

    /**
     * Initial column model. Before rendering, this model holds all registered columns. After rendering, use
     * {@link TreeGrid#getColumnModel()} instead.
     */
    protected ColumnModel columnModel;

    /**
     * A list that stores all newly created, modified and removed models from current treestore.
     */
    protected List<M> changeTrackedModels = new LinkedList<M>();

    /**
     * Formatter for model data. Initially, it's the default one - {@link DefaultTreeGridModelDataFormatter}.
     */
    protected TreeGridModelDataFormatter<M> modelDataFormatter = new DefaultTreeGridModelDataFormatter<M>();

    /**
     * A flag indicating that change-tracking feature should be disabled.
     */
    private boolean changeTrackDisabled;

    /**
     * A flag indicating if all columns should be sortable or not. By default, all columns are <b>NOT sortable</b>.
     */
    private boolean columnsSortable;

    /**
     * A flag indicating if this treegrid is editable or not. This flag must be set before rendering this widget.
     */
    private boolean cellEditable;

    /**
     * Desired selection mode (pre-rendered).
     */
    private SelectionMode selectionMode = SelectionMode.SINGLE;

    /**
     * heading text.
     */
    private String treeHeading;

    /**
     * Tree unique ID. This ID is useful when state management is enabled for this widget.
     */
    private String treeId;

    /**
     * 'stateful' flag.
     */
    private boolean treeStateful;

    /**
     * Model key provider when {@link #treeStateful} = <code>true</code>.
     */
    private ModelKeyProvider<M> modelKeyProvider;

    /**
     * A list that stores all editing editors (which start editing but not finish). This list is useful when all editing
     * values should be saved to this tree's records.
     */
    private Set<Editor> editingEditors = new LinkedHashSet<Editor>();

    private boolean treeEnabled;

    /**
     * A special flag indicating if all components inside this layout container has been rendered inside
     * {@link #onRender(Element, int)}. This flag is reset when this method is called and will be set to
     * <code>true</code> before all components are instantiated and added to this container.
     */
    private boolean componentRendered;

    protected List<ComponentPlugin> componentPlugins = new ArrayList<ComponentPlugin>();

    /**
     * All models filtered by all registered {@link StoreFilter} (See {@link #addStoreFilter(StoreFilter)}) from the
     * last invocation of {@link #applyFilters()}.
     */
    private List<M> storeFilteredModels = new LinkedList<M>();

    /**
     * Constructs this treegrid with a store and a column model.
     * 
     * @param editable
     * @param store
     * @param cm
     */
    public AbstractTreeGrid(final boolean editable, final TreeStore<M> store, final ColumnModel cm) {
        super(true);

        this.cellEditable = editable;
        this.treeStore = store;
        this.columnModel = cm;

        String styleName = getOverridingStyleName();
        if (styleName != null) {
            addStyleName(styleName);
        }
    }

    /**
     * Returns current value of {@link #selectionMode}.
     * 
     * @return the {@link #selectionMode}.
     */
    public SelectionMode getSelectionMode() {
        return selectionMode;
    }

    /**
     * Sets new value for {@link #selectionMode}.
     * 
     * @param selectionMode
     *            the new selectionMode to set
     */
    public void setSelectionMode(SelectionMode selectionMode) {
        this.selectionMode = selectionMode;
    }

    /**
     * Returns current value of {@link #componentRendered}.
     * 
     * @return the {@link #componentRendered}.
     */
    public final boolean isComponentRendered() {
        return componentRendered;
    }

    /**
     * Sets new formatter for model data rendering.
     * <p>
     * <b>REMARK</b>: this method is valuable before rendering this component.
     * 
     * @param formatter
     *            the modelDataFormatter to set.
     */
    public void setModelDataFormatter(final TreeGridModelDataFormatter<M> formatter) {
        if (!isComponentRendered()) {
            this.modelDataFormatter = formatter;
        }
    }

    /**
     * Returns current value of {@link #changeTrackDisabled}.
     * 
     * @return the {@link #changeTrackDisabled}.
     */
    public boolean isChangeTrackDisabled() {
        return changeTrackDisabled;
    }

    /**
     * Sets new value for {@link #changeTrackDisabled}.
     * 
     * @param changeTrackDisabled
     *            the {@link #changeTrackDisabled} to set.
     */
    public void setChangeTrackDisabled(boolean changeTrackDisabled) {
        this.changeTrackDisabled = changeTrackDisabled;
    }

    /**
     * Returns current value of {@link #treeId}.
     * 
     * @return the {@link #treeId}.
     */
    public String getTreeId() {
        return treeId;
    }

    /**
     * Sets new value for {@link #treeId}.
     * 
     * @param treeId
     *            the new treeId to set
     */
    public void setTreeId(String treeId) {
        this.treeId = treeId;
    }

    /**
     * Returns current value of {@link #treeStateful}.
     * 
     * @return the {@link #treeStateful}.
     */
    public boolean isTreeStateful() {
        return treeStateful;
    }

    /**
     * Sets new value for {@link #treeStateful} and use default model key generation (i.e. use
     * {@link AbstractTreeModel#getId()} as the unique key for a model). This enables/disables state management
     * according to given value.
     * 
     * @param treeStateful
     *            the new treeStateful to set
     */
    public void setTreeStateful(boolean treeStateful) {
        setTreeStateful(treeStateful, new DefaultModelKeyProvider<M>());
    }

    /**
     * Sets new value for {@link #treeStateful}. This enables/disables state management according to given value.
     * 
     * @param newValue
     *            The new stateful to set. If it's <code>true</code>, the next argument is used to specify a custom
     *            {@link ModelKeyProvider}.
     * @param customKeyProvider
     *            Custom {@link ModelKeyProvider} for queries to get model unique key. If it's <code>null</code>, the
     *            default provider (i.e. {@link DefaultModelKeyProvider}) is used.
     * @see DefaultModelKeyProvider
     */
    public void setTreeStateful(boolean newValue, ModelKeyProvider<M> customKeyProvider) {
        this.treeStateful = newValue;
        if (newValue) {
            this.modelKeyProvider = customKeyProvider;
        } else {
            this.modelKeyProvider = null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void clearState() {
        super.clearState();

        // also clear the inner tree's state
        this.tree.clearState();
    }

    /**
     * Returns current value of {@link #cellEditable}.
     * 
     * @return the {@link #cellEditable}.
     */
    public boolean isCellEditable() {
        return cellEditable;
    }

    /**
     * Returns current 'expanded' state of this treegrid.
     * 
     * @return Current 'expanded' state (may be empty) or <code>null</code> if there is no 'expanded' state.
     */
    public final List<?> getTreeExpandedState() {
        Object treeState = StateManager.get().get(getTreeId());
        if (treeState != null) {
            Object expandedStates = ((Map<?, ?>) treeState).get("expanded");
            if (expandedStates != null && expandedStates instanceof List<?>) {
                return (List<?>) expandedStates;
            }
        }
        return null;
    }

    /**
     * Returns current value of {@link #columnsSortable}.
     * 
     * @return the {@link #columnsSortable}.
     */
    public boolean isColumnsSortable() {
        return columnsSortable;
    }

    /**
     * Sets new value for {@link #columnsSortable}.
     * <p>
     * <b>REMARK</b>: this method is valuable before rendering this component.
     * 
     * @param columnsSortable
     *            the new columnsSortable to set
     */
    public void setColumnsSortable(boolean columnsSortable) {
        if (!isComponentRendered()) {
            this.columnsSortable = columnsSortable;
        }
    }

    /**
     * Returns current row count.
     * 
     * @return The number of records in current tree store or {@link Integer#MIN_VALUE} if current tree hasn't been
     *         instantiated.
     */
    public final int getRowCount() {
        if (tree != null) {
            return tree.getStore().getModels().size();
        }
        return Integer.MIN_VALUE;
    }

    /**
     * Returns current column model of this treegrid.
     * 
     * @return If this widget creates {@link #tree} component already, return the column model attached to the
     *         {@link #tree}. If not, then return {@link #columnModel}.
     */
    public ColumnModel getColumnModel() {
        if (tree != null) {
            return tree.getColumnModel();
        } else {
            return columnModel;
        }
    }

    /**
     * Checks if a model is a leaf model.
     * <p>
     * In fact, this method is useful only after this widget had been rendered.
     * 
     * @param model
     * @return <code>true</code> if this treegrid has been rendered and given model is leaf.
     */
    public final boolean isLeaf(M model) {
        if (isComponentRendered()) {
            return tree.isLeaf(model);
        }
        return false;
    }

    /**
     * Returns all model data of this tree.
     * 
     * @return
     */
    public List<M> getCurrentModels() {
        TreeStore<M> treestore = getCurrentTreeStore();
        return new ArrayList<M>(treestore.getModels());
    }

    /**
     * Sets new models (i.e. replaces old models if some exist) for this treegrid.
     * 
     * @param newModels
     */
    public void setCurrentModels(List<M> newModels) {
        this.changeTrackDisabled = true;

        TreeStore<M> treestore = getCurrentTreeStore();
        treestore.removeAll();
        treestore.add(newModels, true);

        this.changeTrackDisabled = false;
    }

    /**
     * Sets new models (i.e. replaces old models if some exist) for this treegrid starting from given root model.
     * 
     * @param newRootModel
     */
    public void setCurrentModels(M newRootModel) {
        setCurrentModels(newRootModel, false);
    }

    /**
     * Sets new models (i.e. replaces old models if some exist) for this treegrid starting from given root model.
     * 
     * @param newRootModel
     * @param onlyChildren
     *            Only childrens of given root model are set to current tree store.
     */
    @SuppressWarnings("unchecked")
    public void setCurrentModels(M newRootModel, boolean onlyChildren) {
        this.changeTrackDisabled = true;

        TreeStore<M> treestore = getCurrentTreeStore();
        treestore.removeAll();

        if (onlyChildren) {
            treestore.add((List) newRootModel.getChildren(), true);
        } else {
            treestore.add(newRootModel, true);
        }

        this.changeTrackDisabled = false;
    }

    /**
     * Returns the currently selected model. This method is applicable for treegrids allowing <i>single-selection</i>.
     * 
     * @return If this tree is cell-editable, the currently focused cell model will be returned. Otherwise, the model of
     *         the selected row is selected.
     */
    public M getSelectedModel() {
        if (cellEditable) {
            CellSelectionModel<M> selectionModel = (CellSelectionModel<M>) tree.getSelectionModel();
            return selectionModel.getSelectCell().model;
        } else {
            return tree.getSelectionModel().getSelectedItem();
        }
    }

    /**
     * Returns the currently selected models. This method is applicable for treegrids allowing
     * <i>multiple-selection</i>.
     * 
     * @return If this tree is cell-editable, the currently focused cell model will be returned. Otherwise, the model of
     *         the selected rows are selected.
     */
    public List<M> getSelectedModels() {
        if (cellEditable) {
            CellSelectionModel<M> selectionModel = (CellSelectionModel<M>) tree.getSelectionModel();
            return Arrays.asList(selectionModel.getSelectCell().model);
        } else {
            return tree.getSelectionModel().getSelectedItems();
        }
    }

    /**
     * Returns current {@link #tree} as an {@link ExtendedEditorTreeGrid}.
     * 
     * @return {@link #tree} or <code>null</code> if this treegrid is not editable.
     */
    protected final EditorTreeGrid<M> getEditorTree() {
        if (isCellEditable()) {
            return (EditorTreeGrid<M>) getTree();
        } else {
            return null;
        }
    }

    /**
     * Returns the inner treegrid of this container.
     * 
     * @return
     */
    protected final TreeGrid<M> getTree() {
        return tree;
    }

    /**
     * Expands all nodes.
     * <p>
     * <b>NOTE</b>: This method is affectable only after this tree grid is rendered.
     */
    public void expandAllNodes() {
        if (isComponentRendered()) {
            tree.expandAll();
        }
    }

    /**
     * Collapses all nodes.
     * <p>
     * <b>NOTE</b>: This method is affectable only after this tree grid is rendered.
     */
    public void collapseAllNodes() {
        if (isComponentRendered()) {
            tree.collapseAll();
        }
    }

    /**
     * Expands a model.
     * <p>
     * <b>NOTE</b>: This method is affectable only after this tree grid is rendered.
     * 
     * @param model
     * @param recursiveExpanded
     *            <code>true</code> to expand recursively all child nodes.
     */
    public void expandModel(M model, boolean recursiveExpanded) {
        if (isComponentRendered()) {
            tree.setExpanded(model, true, recursiveExpanded);
        }
    }

    /**
     * Expands all models satisfying given predicate.
     * <p>
     * <b>NOTE</b>: This method is affectable only after this tree grid is rendered.
     * 
     * @param predicate
     *            Predicate which defines all accepted criteria for candidate-model selection.
     */
    public void expandModels(Predicate<M> predicate) {
        if (isRendered() && predicate != null) {
            List<M> allModels = getCurrentModels();
            for (M model : allModels) {
                if (predicate.evaluate(model)) {
                    expandModel(model, false);
                }
            }
        }
    }

    /**
     * Expands the first root node of this tree <b>and all its children in all depth</b> if this root node has some
     * children.
     */
    public void expandFirstRootModel() {
        expandFirstRootModel(true);
    }

    /**
     * Expands the first root node of the tree.
     * 
     * @param recursiveExpanded
     *            <code>true</code> to expand recursively all child nodes.
     */
    public void expandFirstRootModel(boolean recursiveExpanded) {
        if (isComponentRendered()) {
            for (M rootNode : treeStore.getRootItems()) {
                if (rootNode.getChildCount() > 0) {
                    expandModel(rootNode, recursiveExpanded);
                }
            }
        }
    }

    /**
     * Deferred-expands all nodes.
     * <p>
     * <b>NOTE</b>: This method is affectable only after this tree grid is rendered AND this command is registered to be
     * executed later.
     */
    public void deferExpandAllNodes() {
        scheduleExpandCommandWithoutExpandedState(new Command() {
            @Override
            public void execute() {
                expandAllNodes();
            }
        });
    }

    /**
     * Deferred-expands all models satisfying given predicate.
     * <p>
     * <b>NOTE</b>: This method is affectable only after this tree grid is rendered AND this command is registered to be
     * executed later.
     * 
     * @param predicate
     *            Predicate which defines all accepted criteria for candidate-model selection.
     */
    public void deferExpandModels(final Predicate<M> predicate) {
        scheduleExpandCommandWithoutExpandedState(new Command() {
            @Override
            public void execute() {
                expandModels(predicate);
            }
        });
    }

    /**
     * Expands the first root node of the tree <b>after all events have been processed (i.e. deferred one)</b>.
     * 
     * @param recursiveExpanded
     *            <code>true</code> to expand recursively all child nodes.
     */
    public void deferExpandFirstRootModels(final boolean recursiveExpanded) {
        scheduleExpandCommandWithoutExpandedState(new Command() {
            @Override
            public void execute() {
                expandFirstRootModel(recursiveExpanded);
            }
        });
    }

    /**
     * Adds a new {@link StoreFilter} to current tree store.
     * 
     * @param filter
     */
    public final void addStoreFilter(final StoreFilter<M> filter) {
        GwtAssert.ARGUMENTS.notNull(filter, "Registered StoreFilter must not be null");

        StoreFilter<M> filterDecorator = new StoreFilter<M>() {
            @Override
            public boolean select(Store<M> store, M parent, M item, String property) {
                boolean selected = filter.select(store, parent, item, property);
                item.setFiltered(!selected);
                if (!selected) {
                    storeFilteredModels.add(item);
                }
                return selected;
            }
        };

        getCurrentTreeStore().addFilter(filterDecorator);
    }

    /**
     * Applies all registered filters to current treegrid.
     */
    public final void applyFilters() {

        // clear 'filtered' state first
        List<M> allModels = getCurrentModels();
        for (M m : allModels) {
            m.setFiltered(false);
        }
        storeFilteredModels.clear();

        // apply filter
        getCurrentTreeStore().applyFilters(null);
    }

    /**
     * Returns the 'enabled' state of the inner tree of this panel.
     * 
     * @return
     */
    @Override
    public boolean isEnabled() {
        return treeEnabled;
    }

    /**
     * Enables/disables the inner tree of this panel.
     * 
     * @param enabled
     *            the enabled state.
     */
    @Override
    public void setEnabled(final boolean enabled) {

        // this config is enough thanks to the job of event 'BeforeEdit'.
        this.treeEnabled = enabled;
    }

    /**
     * Returns row index of given model.
     * <p>
     * <b>NOTE</b>: This method is affectable only after this tree grid is rendered.
     * 
     * @param model
     *            Model data to look for row.
     * @return Row index, starting from 0, or <b>-1</b> if this treegrid hasn't been rendered.
     */
    public int findRowIndex(M model) {
        if (isComponentRendered()) {
            return tree.getView().findRowIndex(tree.getView().getRow(model));
        } else {
            return -1;
        }
    }

    /**
     * Checks if this tree is dirty - i.e. this tree has some changes - or not.
     * 
     * @return <code>true</code> if this tree is rendered and has some dirty records.
     */
    public boolean isDirty() {
        if (!changeTrackDisabled) {
            return !changeTrackedModels.isEmpty();
        }

        // this check is applied for trees that don't use 'change-tracking' mechanism
        return isComponentRendered() && StoreUtils.isDirty(treeStore);
    }

    /**
     * Checks if this tree is valid or not - i.e. all changes in all cells are valid.
     * 
     * @return <code>true</code> if this tree is rendered and contains only valid cells.
     */
    public final boolean isValid() {
        return isComponentRendered() && StoreUtils.isValid(treeStore);
    }

    /**
     * Sets 'visible' state (i.e. hidden or shown) for given column.
     * 
     * @param columnIndex
     *            Column index, starting from 0.
     * @param visible
     */
    public void setColumnVisible(int columnIndex, boolean visible) {
        if (isComponentRendered()) {
            tree.getColumnModel().setHidden(columnIndex, !visible);
        }
    }

    /**
     * Returns tree row height.
     * 
     * @return Row height of the treeview of the inner tree or -1 if this widget hasn't been rendered.
     */
    public final int getRowHeight() {
        if (isComponentRendered()) {
            return tree.getTreeView().getRowHeight();
        }
        return -1;
    }

    /**
     * Returns current list store.
     * 
     * @return
     */
    public final ListStore<M> getCurrentStore() {
        if (isComponentRendered()) {
            return tree.getStore();
        }
        return null;
    }

    /**
     * Returns current treestore of this treegrid.
     * 
     * @return if this treegrid has been rendered, the store contained in {@link #tree} will be returned. Otherwise, the
     *         {@link #treeStore} is returned.
     */
    public final TreeStore<M> getCurrentTreeStore() {
        TreeStore<M> treestore = this.treeStore;
        if (isComponentRendered()) {
            treestore = tree.getTreeStore();
        }
        return treestore;
    }

    /**
     * Add new blank model with given parent.
     * 
     * @param parent
     * @return
     */
    public M addBlankModel(M parent) {
        M newModel = createNewModel(parent);
        addModel(parent, newModel, false);
        return newModel;
    }

    /**
     * Adds new model into current tree under given parent model.
     * 
     * @param parent
     * @param newModel
     * @param addChildren
     */
    public void addModel(M parent, M newModel, boolean addChildren) {
        if (newModel != null) {
            newModel.setParent(parent);
            getCurrentTreeStore().add(parent, newModel, addChildren);

            // After adding this model using treestore, the original model hasn't been changed -> add by hand.
            parent.add(newModel);
        }
    }

    /**
     * Inserts new model into current tree under given parent model.
     * 
     * @param parent
     * @param newModel
     * @param index
     * @param addChildren
     */
    public void insertModel(M parent, M newModel, int index, boolean addChildren) {
        if (newModel != null) {
            newModel.setParent(parent);
            getCurrentTreeStore().insert(parent, newModel, index, addChildren);

            parent.insert(newModel, index);
        }
    }

    /**
     * Removes a model from current treegrid.
     * 
     * @param model
     */
    public final void removeModel(M model) {
        getCurrentTreeStore().remove(model);

        // After removing this model using treestore, the original model hasn't been changed -> remove by hand.
        if (model.getParent() != null) {
            model.getParent().remove(model);
        }
    }

    /**
     * Sets heading text for this treegrid.
     * 
     * @param text
     * @return
     */
    public void setHeading(String text) {
        if (isComponentRendered() && treeContentPanel.isHeaderVisible()) {
            treeContentPanel.setHeading(text);
        } else {
            treeHeading = text;
        }
    }

    /**
     * Returns current models which have been tracking for changes <b>AND</b> <i>removes all tracked models from current
     * change-tracked models</i>.
     * 
     * @return a new list of all change-tracked models.
     */
    public List<M> pollChangeTrackedModels() {
        List<M> results = new ArrayList<M>(changeTrackedModels);
        changeTrackedModels.clear();
        return results;
    }

    /**
     * Returns current models which have been tracking for changes.
     * 
     * @return a new list of all change-tracked models.
     */
    public List<M> getChangeTrackedModels() {
        return new ArrayList<M>(changeTrackedModels);
    }

    /**
     * Finishes all editing cells and clears current {@link #editingEditors}. This method is useful before saving this
     * tree to assure that all editing cells complete editing.
     */
    public final void finishAllEditingCells() {
        for (Editor editor : editingEditors) {
            editor.completeEdit();
        }
        editingEditors.clear();
    }

    /**
     * Returns current {@link GridSelectionModel}. Depending on 'editable' attribute of this treegrid, the returned
     * selection model will be either {@link com.extjs.gxt.ui.client.widget.treegrid.TreeGridSelectionModel} or
     * {@link com.extjs.gxt.ui.client.widget.treegrid.CellTreeGridSelectionModel}.
     * 
     * @return
     */
    public GridSelectionModel<M> getGridSelectionModel() {
        if (isComponentRendered()) {
            return tree.getSelectionModel();
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void focus() {
        super.focus();
        if (isComponentRendered()) {
            tree.focus();
        }
    }

    /**
     * Returns a single style name for CSS overriding for this treegrid.
     * 
     * @return
     */
    protected String getOverridingStyleName() {
        return null;
    }

    /**
     * Creates new blank model.
     * 
     * @param parent
     * @return A new blank model for insertion via {@link #addBlankModel(AbstractTreeModel)}. If it's <code>null</code>,
     *         nothing is added.
     */
    protected M createNewModel(M parent) {
        return null;
    }

    /**
     * Creates a {@link ContentPanel} that contains the created tree stored in {@link #tree}, which is created before
     * this method is called.
     * 
     * @return
     */
    protected ContentPanel createContentPanel() {
        ContentPanel contentPanel = new ContentPanel();
        contentPanel.setBodyBorder(false);
        contentPanel.setButtonAlign(HorizontalAlignment.CENTER);
        contentPanel.setLayout(new FitLayout());
        contentPanel.setHeading(treeHeading);
        return contentPanel;
    }

    /**
     * Configures attributes on rendering this component.
     * 
     * @param parent
     *            the target element.
     * @param index
     *            the insert location.
     */
    protected void configureOnRendering(final Element parent, final int index) {
        setLayout(new FitLayout());
    }

    /**
     * Creates new instance of the inner {@link TreeGrid}.
     * 
     * @return
     */
    protected TreeGrid<M> createTreeGrid() {

        // create treegrid
        TreeGrid<M> treeGrid;
        if (cellEditable) {
            treeGrid = createEditableTreeGrid();
            GwtAssert.STATE.isTrue(treeGrid instanceof ExtendedEditorTreeGrid<?>,
                    "Only ExtendedEditorTreeGrid or its subclasses is acceptable");
        } else {
            treeGrid = createNonEditableTreeGrid();
        }

        // with custom gridview if any
        GridView gridView = createCustomView();
        if (gridView != null && gridView instanceof ExtendedTreeGridView<?>) {
            treeGrid.setView(gridView);
        }

        // and with custom cell selection model if any
        CellSelectionModel<M> selectionModel = createCustomSelectionModel();
        if (selectionModel != null) {
            treeGrid.setSelectionModel(selectionModel);
        }

        // apply state management if specified
        treeGrid.setStateful(isTreeStateful());
        treeStore.setKeyProvider(modelKeyProvider);

        // use custom tree id
        if (GwtStringUtils.isBlank(treeId)) {
            treeId = GwtObjectUtils.identityToString(this);
        }
        treeGrid.setId(treeId);

        return treeGrid;
    }

    protected ExtendedEditorTreeGrid<M> createEditableTreeGrid() {
        return new ExtendedEditorTreeGrid<M>(treeStore, columnModel);
    }

    protected TreeGrid<M> createNonEditableTreeGrid() {
        return new ExtendedTreeGrid<M>(treeStore, columnModel);
    }

    protected GridView createCustomView() {
        return null;
    }

    protected CellSelectionModel<M> createCustomSelectionModel() {
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onRender(Element parent, int index) {
        super.onRender(parent, index);

        componentRendered = false;

        // configure this container
        configureOnRendering(parent, index);

        // init (or re-init) all column renderers
        initializeColumns();

        // create tree
        tree = createTreeGrid();

        customizeTreeAttributes();

        tree.getSelectionModel().setSelectionMode(selectionMode);
        for (ComponentPlugin plugin : this.componentPlugins) {
            tree.addPlugin(plugin);
        }

        customizeColumnRenderers();

        // create content panel.
        treeContentPanel = createContentPanel();
        treeContentPanel.add(tree);

        add(treeContentPanel);

        componentRendered = true;
    }

    /**
     * This method allow sub-classs to customize various attributes of the tree.
     */
    protected void customizeTreeAttributes() {
        // with Auto height, the the vertical srcollbar work worse
        tree.setAutoHeight(false);
        tree.setBorders(false);
        tree.setExpandOnFilter(false);
    }

    /**
     * Customly handles after a new model is added to current treegrid.
     * 
     * @param event
     * @param addedModel
     */
    protected void afterAddingModel(TreeStoreEvent<M> event, M addedModel) {
        // nothing special handling code
    }

    /**
     * Customly handles after a new model is removed from current treegrid.
     * 
     * @param event
     * @param removedModel
     */
    protected void afterRemovingModel(TreeStoreEvent<M> event, M removedModel) {
        // nothing special handling code
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void afterRender() {
        super.afterRender();
        initListeners();
    }

    /**
     * Performs all checks before removing given model from current tracking list.
     * 
     * @param removedModel
     * @return <code>true</code> to continue removing this model.
     */
    protected boolean beforeRemovingFromTrackingList(M removedModel) {
        return true;
    }

    /**
     * Performs all checks before updating given model to current tracking list.
     * 
     * @param updatingModel
     *            Updating model.
     * @return <code>true</code> to continue updating this model.
     */
    protected boolean beforeUpdatingToTrackingList(M updatingModel) {
        return true;
    }

    /**
     * Performs all checks before adding given model to current tracking list.
     * 
     * @param addingModel
     * @return <code>true</code> to continue adding this model.
     */
    protected boolean beforeAddingToTrackingList(M addingModel) {
        return true;
    }

    /**
     * Handles before a cell is about to edit.
     * <p>
     * By default, the cell editing is disabled if this tree is disabled (i.e. {@link #treeEnabled} = <code>false</code>
     * ). Thus, while subclasses may override this method for custom checks, they MUST call this class's code. For
     * example: <code>super.beforeCellEditing(e);</code>.
     * 
     * @param e
     * @return
     */
    protected boolean beforeCellEditing(GridEvent<M> e) {

        // enable/disable the tree according to user choice
        return treeEnabled;
    }

    protected void afterCellEditing(GridEvent<M> e) {
        // nothing to do
    }

    /**
     * Initializes all columns before the tree is created.
     */
    private void initializeColumns() {
        for (ColumnConfig col : columnModel.getColumns()) {
            col.setSortable(columnsSortable);
        }
        initializeColumnRenderers();
    }

    /**
     * Initializes all column editors if any.
     */
    protected void initializeColumnEditors() {
        // nothing special here
    }

    /**
     * Customizes column renderers after the tree has been created.
     * <p>
     * Subclasses may override this method to customize renderers. However, it's necessary that the call from the
     * <code>super</code> class MUST BE INVOKED.
     */
    protected void customizeColumnRenderers() {
        // the initialization of editors must be after renderers because in some cases we need to build editors base on
        // renderers.
        initializeColumnEditors();
    }

    protected void initListeners() {
        initCellEditingListeners();
        initTreeStoreChangeListeners();

        // Add a listener to recursively remove 'expanded' state of a collapsed node.
        // NOTE: This is a hack of GXT 2.1.1 because on collapsing a node, only that node is removed from 'expanded'
        // state.
        // REASON:
        // For example: If a tree has node A, which contains 2 nodes A1 and A2. Node A1 has 2 nodes: A11 and A12. At one
        // time, all nodes are expanded => 'expanded' state contains all the nodes. Then, node A is collapsed. At this
        // time, only node A is removed from current 'expanded'. Thus, on restoring the state of this tree, all nodes
        // are still expanded.
        // TODO LNT [Jun 17, 2010]: look out for an update of GXT to fix this problem.
        tree.addListener(Events.Collapse, new Listener<TreeGridEvent<M>>() {
            @Override
            public void handleEvent(TreeGridEvent<M> evt) {
                removeModelFromExpandedState(evt.getModel());
            }
        });
    }

    /**
     * Creates new tree grid cell renderer. This renderer is used for all cells in auto-expanded column (specified in
     * {@link #setAutoExpandColumn(String)}) or in the first column of the tree grid.
     * 
     * @return new instance of {@link TreeGridCellRenderer}.
     */
    protected TreeGridCellRenderer<M> createTreeGridCellRenderer() {
        return new DefaultTreeGridCellRenderer<M>(modelDataFormatter);
    }

    /**
     * Creates new normal grid cell renderer. This renderer is used for all cells except for those in auto-expanded
     * column (specified in {@link #setAutoExpandColumn(String)}) or in the first column of the tree grid.
     * 
     * @param column
     * @return new instance of {@link GridCellRenderer}.
     */
    protected GridCellRenderer<M> createNormalGridCellRenderer(ColumnConfig column) {
        return new DefaultGridCellRenderer<M>(modelDataFormatter);
    }

    /**
     * Searches for the treeview column (usually the 1st one).
     * 
     * @return
     */
    protected final ColumnConfig findTreeViewColumn() {
        ColumnModel columnModel = getColumnModel();
        if (columnModel.getColumnCount() > 0) {
            return columnModel.getColumn(0);
        }
        return null;
    }

    private void initializeColumnRenderers() {
        ColumnConfig treeViewColumn = findTreeViewColumn();

        // set a default renderer for all columns
        for (ColumnConfig column : this.columnModel.getColumns()) {
            if (column == treeViewColumn) {

                // now because of the nature of a treegrid that the first column renderer must be a
                // TreeGridCellRenderer, find and set new renderer for this column.
                GridCellRenderer<M> cellRenderer = createTreeGridCellRenderer();
                if (cellRenderer != null) {
                    treeViewColumn.setRenderer(cellRenderer);
                }

            } else {

                GridCellRenderer<M> cellRenderer = null;
                cellRenderer = createNormalGridCellRenderer(column); // render normal cell

                if (cellRenderer != null) {
                    column.setRenderer(cellRenderer);

                    // component plugins should be added to current list for 'onRender' method
                    if (cellRenderer instanceof ComponentPlugin) {
                        componentPlugins.add((ComponentPlugin) cellRenderer);
                    }
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void initCellEditingListeners() {

        // add pre/post cell-editing events in TREE level
        tree.addListener(Events.BeforeEdit, new Listener<BaseEvent>() {
            @Override
            public void handleEvent(BaseEvent be) {
                if (be.getSource() == tree) {
                    boolean continued = beforeCellEditing((GridEvent<M>) be);
                    be.setCancelled(!continued);
                }
            }
        });
        tree.addListener(Events.AfterEdit, new Listener<BaseEvent>() {
            @Override
            public void handleEvent(BaseEvent be) {
                if (be.getSource() == tree) {
                    afterCellEditing((GridEvent<M>) be);
                }
            }
        });

        Listener<EditorEvent> cellEditorListener = new Listener<EditorEvent>() {
            public void handleEvent(EditorEvent event) {
                if (event.getType() == Events.StartEdit) {
                    editingEditors.add(event.getEditor());
                } else {
                    editingEditors.remove(event.getEditor());
                }
            }
        };
        for (ColumnConfig column : getColumnModel().getColumns()) {
            if (column.getEditor() != null) {
                CellEditor editor = column.getEditor();
                editor.addListener(Events.StartEdit, cellEditorListener);
                editor.addListener(Events.CancelEdit, cellEditorListener);
                editor.addListener(Events.Complete, cellEditorListener);
            }
        }
    }

    private void initTreeStoreChangeListeners() {

        // add listeners for add/remove/update to update change-track list
        final TreeStore<M> treestore = getCurrentTreeStore();
        treestore.addListener(Store.Remove, new Listener<TreeStoreEvent<M>>() {
            @Override
            public void handleEvent(TreeStoreEvent<M> event) {
                if (event.getSource() == treestore) {
                    M removedModel = event.getChild();

                    // allow subclasses to determine when to remove
                    if (!beforeRemovingFromTrackingList(removedModel)) {
                        return;
                    }

                    addModelToTrackingList(removedModel, Store.Remove);

                    List<M> removedChildModels = event.getChildren();
                    for (M m : removedChildModels) {
                        addModelToTrackingList(m, Store.Remove);
                    }

                    // allow custom handling
                    afterRemovingModel(event, removedModel);
                }
            }
        });
        treestore.addListener(Store.Add, new Listener<TreeStoreEvent<M>>() {
            @Override
            public void handleEvent(TreeStoreEvent<M> event) {
                if (event.getSource() == treestore) {
                    M addedModel = event.getChildren().get(0);

                    // allow subclasses to determine when to add
                    if (!beforeAddingToTrackingList(addedModel)) {
                        return;
                    }

                    addModelToTrackingList(addedModel, Store.Add);

                    // allow custom handling
                    afterAddingModel(event, addedModel);
                }
            }
        });
        treestore.addListener(Store.Update, new Listener<StoreEvent<M>>() {
            @Override
            public void handleEvent(StoreEvent<M> event) {
                if (event.getSource() == treestore && event.getOperation() == RecordUpdate.EDIT) {
                    M modifiedModel = event.getModel();

                    // allow subclasses to determine when to remove
                    if (!beforeUpdatingToTrackingList(modifiedModel)) {
                        return;
                    }

                    addModelToTrackingList(modifiedModel, Store.Update);
                }
            }
        });
    }

    private void scheduleExpandCommandWithoutExpandedState(Command command) {
        if (!hasExpandedState()) {
            // NOTE: I cannot use DeferredCommand because it doesn't work sometimes without a clear reason. Thus, I have
            // to schedule a timer once to perform this task.
            // REMARK: this auto-expanding should be performed only if this tree has no state
            DeferredCommandScheduler.scheduleOnce(command, 1);
        }
    }

    private boolean hasExpandedState() {
        return getTreeExpandedState() != null;
    }

    private void removeModelFromExpandedState(M model) {
        if (tree.isStateful() && treeStore.getKeyProvider() != null) {
            Map<String, Object> state = tree.getState();

            @SuppressWarnings("unchecked")
            List<String> expanded = (List) state.get("expanded");

            // remove this model from current 'expanded' states
            String id = treeStore.getKeyProvider().getKey(model);
            if (expanded != null && expanded.contains(id)) {
                expanded.remove(id);
            }

            // remove all children from current 'expanded' state
            for (ModelData m : model.getChildren()) {
                @SuppressWarnings("unchecked")
                M childModel = (M) m;
                removeModelFromExpandedState(childModel);
            }

            tree.saveState();
        }
    }

    private void addModelToTrackingList(M model, EventType eventType) {

        // don't do anything if change-tracking is disabled
        if (changeTrackDisabled) {
            return;
        }

        int trackedModelIndex = changeTrackedModels.indexOf(model);

        if (eventType == Store.Add) {
            model.setState(ChangeState.NEW);
            changeTrackedModels.add(model);

        } else if (eventType == Store.Remove) {
            if (trackedModelIndex >= 0) {
                M trackedModel = changeTrackedModels.get(trackedModelIndex);
                GwtAssert.STATE.isTrue(trackedModel.getState() != ChangeState.REMOVED,
                        "Remove a REMOVED model is strange!!");

                if (trackedModel.getState() == ChangeState.NEW) {
                    changeTrackedModels.remove(trackedModelIndex);
                } else { // for MODIFIED or UNCHANGED model
                    trackedModel.setState(ChangeState.REMOVED);
                }
            } else {
                model.setState(ChangeState.REMOVED);
                changeTrackedModels.add(model);
            }

        } else if (eventType == Store.Update) {
            if (trackedModelIndex >= 0) {
                M trackedModel = changeTrackedModels.get(trackedModelIndex);
                GwtAssert.STATE.isTrue(trackedModel.getState() != ChangeState.REMOVED,
                        "Remove a REMOVED model is strange!!");

                if (trackedModel.getState() == ChangeState.NEW) {
                    // do nothing because this model is new
                } else { // for MODIFIED or UNCHANGED model
                    trackedModel.setState(ChangeState.MODIFIED);
                }
            } else {
                model.setState(ChangeState.MODIFIED);
                changeTrackedModels.add(model);
            }

        } else {
            // nothing to do
        }
    }

    private static final class DefaultModelKeyProvider<M extends AbstractTreeModel> implements ModelKeyProvider<M> {
        @Override
        public String getKey(M model) {
            return model.getId();
        }
    }
}
